/**
 * SPRINGBACK
 * A Java implementation of the Formspring API.
 * 
 * Distributed under the MIT License. See LICENSE for more information.
 */
package com.mattsfarmer.springback;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.scribe.http.Request;
import org.scribe.http.Response;
import org.scribe.http.Request.Verb;
import org.scribe.oauth.Scribe;
import org.scribe.oauth.Token;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;

/**
 * This class handles generation of and interpretation of
 * Formspring API messages in a transparent, efficient way.
 * @author Matt Farmer
 * @version 1.0
 */
public class FormspringAPI {
	///////
	// CLASS CONSTANTS
	///////
	private static final String apiUrl = "http://beta-api.formspring.me";
	private static final String requestTokenUrl = "http://www.formspring.me/oauth/request_token";
	private static final String authorizeUrl = "http://www.formspring.me/oauth/authorize";
	private static final String accessTokenUrl = "http://www.formspring.me/oauth/access_token";
	private static String consumerKey = "CHANGEME";
	private static String consumerSecret = "CHANGEME";
	
	///////
	// CLASS MEMBERS
	///////
	//private OAuthConsumer consumer;
	//private OAuthProvider provider;
	private Serializer serializer;
	private boolean isAuthed;
	private Scribe scribeObj;
	private Token requestToken;
	private Token accessToken;
	private String currentUsersName;
	private FormspringPerson currentUser;
	private boolean isCaching;
	private String cacheDir;
	
	public FormspringAPI(String consumerKey, String consumerSecret) {
		//Is authed?
		isAuthed = false;
		
		//Save the key and secret
		this.consumerKey = consumerKey;
		this.consumerSecret = consumerSecret;
		
		//Init the XML serializer
		serializer = new Persister();
		
		//Create the Scribe Properties.
		Properties scribeProps = new Properties();
		scribeProps.put("consumer.key", consumerKey);
		scribeProps.put("consumer.secret", consumerSecret);
		scribeProps.put("request.token.verb", "POST");
		scribeProps.put("request.token.url", requestTokenUrl);
		scribeProps.put("access.token.verb", "POST");
		scribeProps.put("access.token.url", accessTokenUrl);
		scribeProps.put("callback.url", "oob");
		scribeProps.put("scribe.equalizer", "com.mattsfarmer.springback.FormspringEqualizer");
		
		//Create the scribe object
		scribeObj = new Scribe(scribeProps);
	}
	
	public FormspringAPI(String consumerKey, String consumerSecret, Token accessToken) {
		//Call the default constructor to get set up
		this(consumerKey, consumerSecret);
		
		//Declare ourselves as authed and set the accessToken
		isAuthed = true;
		this.accessToken = accessToken;
		
		//Run an API call to retrieve profile information on the current user.
		FormspringAPIResponse response = this.profileDetails(null);
		this.currentUser = response.getProfile().get(0);
		this.currentUsersName = response.getProfile().get(0).getName();
	}
	
	public void setCacheDir(String cacheDir) {
		if(cacheDir == null) {
			this.cacheDir = null;
			this.isCaching = false;
			return;
		}
		
		this.cacheDir = cacheDir;
		this.isCaching = true;
	}
	
	public String initAuthorize() {
		try {
			//Output debug message.
			System.out.println("[FSAPI] Initilizing request authorization....");
			
			//Fetch the request token and token secret.
			//String authUrl = provider.retrieveRequestToken(consumer, OAuth.OUT_OF_BAND);
			requestToken = scribeObj.getRequestToken();
			System.out.println("[FSAPI] Request authorization successful.");
			return authorizeUrl + "?oauth_token=" + requestToken.getToken();
		} catch(Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}
	
	public boolean authorize(String pin) {
		System.out.println("[FSAPI] Performing user authorization....");
		
		try {
			accessToken = scribeObj.getAccessToken(requestToken, pin);
			this.isAuthed = true;
			System.out.println("[FSAPI] User authorized... " + accessToken.getToken());
			
			//Run an API call to retrieve profile information on the current user.
			FormspringAPIResponse response = this.profileDetails(null);
			this.currentUser = response.getProfile().get(0);
			this.currentUsersName = response.getProfile().get(0).getName();
			return true;
		} catch(Exception e) {
			e.printStackTrace();
		}
		
		return false;
	}
	
	/**
	 * Queries the FormspringAPI for the number of questions a specific user has answered.
	 * @param username The user to query the API about.
	 * @return A FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse answeredCount(String username) {
		//Formulate the url to call
		String url = "/answered/count/" + username + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI for the details of a specific question a user
	 * has answered.
	 * @param username The user who answered the question.
	 * @param itemId The ID of the question.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse answeredDetails(String username, int itemId) {
		//Formulate the url to call
		String url = "/answered/details/" + username + "/" + itemId + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI for the list of questions a user has answered.
	 * @param username The user account to query.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse answeredList(String username) {
		//Formulate the url to call
		String url = "/answered/list/" + username + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to delete a specific answer off of the
	 * currently authorized user's profile. AUTHORIZATION REQUIRED.
	 * @param itemId The answer to delete.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse answeredRemove(int itemId) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/answered/remove/" + itemId + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to follow a specific user from the
	 * currently authorized account. ATUHORIZATION REQUIRED.
	 * @param username The user to follow.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followAdd(String username) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/add/" + username + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to retrieve the last 20 answers given by
	 * followed users. AUTHORIZATION REQUIRED.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followAnswers() {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/answers.xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to retrieve the 20 answers given by followed
	 * users answered before the answer with the id beforeId. AUTHORIZATION REQUIRED.
	 * @param beforeId The id of the question to serve as the time boundary.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followAnswers(int beforeId) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/answers.xml?before=" + beforeId;
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI for the number of users the current user
	 * follows. AUTHORIZATION REQUIRED.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followCount() {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/count.xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI for the first 20 of the list of users who this user is following.
	 * AUTHORIZATION REQUIRED.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followList() {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/list.xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI for a specific page from the list of users the
	 * current user follows. AUTHORIZATION REQUIRED.
	 * @param page The page number to request.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followList(int page) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/list.xml?page=" + page;
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to remove a user from the current user's follow
	 * list. AUTHORIZATION REQUIRED.
	 * @param username The user to remove from your follow list.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse followRemove(String username) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/follow/remove/" + username + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to remove an item from the current user's inbox and block
	 * all further questions from the user who wrote the question. AUTHORIZATION REQUIRED.
	 * @param itemId The id of the question.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse inboxBlock(int itemId, String reason) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/block/" + itemId + ".xml";
		
		//Build the Map of POST key/values
		Map <String, String> postMap = new HashMap<String,String>();
		postMap.put("reason", reason);
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, true, postMap);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to retrieve the count of the number of questions in
	 * the current user's inbox. AUTHORIZATION REQUIRED.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse inboxCount() {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/count.xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to list the questions present in the current user's inbox.
	 * This method will only show the first 20 questions. AUTHORIZATION REQUIRED.
	 * @return The FormspringAPIResponse with the result of the API call.
	 */
	public FormspringAPIResponse inboxList() {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/list.xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to list a page from the question in the user's inbox.
	 * AUTHORIZATION REQUIRED.
	 * @param page The page number to retrieve.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse inboxList(int page) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/list.xml?page=" + page;
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to add a random question to the user's inbox.
	 * AUTHORIZATION REQUIRED.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse inboxRandom() {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/random.xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to remove a question from the user's inbox.
	 * @param itemId The ID of the question to remove.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse inboxRemove(int itemId) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/remove/" + itemId + ".xml";
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Submits a response to a question in the user's inbox to the FormspringAPI.
	 * @param itemId The question to respond to. AUTHORIZATION REQUIRED.
	 * @param response The response to send.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse inboxRespond(int itemId, String response) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/inbox/respond/" + itemId + ".xml";
		
		//Create a hash map with the key/value pairs for the POST method.
		Map<String,String> postMap = new HashMap<String,String>();
		postMap.put("response", response);
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, true, postMap);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Submits a question to a user for them to answer. AUTHORIZATION REQUIRED.
	 * @param username The username of the target user.
	 * @param question The question to ask the user.
	 * @param anonymous True for anonymous questions, false otherwise.
	 * @return
	 */
	public FormspringAPIResponse profileAsk(String username, String question, boolean anonymous) {
		//Check authorization
		if(this.isAuthed == false) return null;
		
		//Formulate the url to call
		String url = "/profile/ask/" + username + ".xml";
		
		//Create a hash map with the key/value pairs for the POST method.
		Map<String,String> postMap = new HashMap<String,String>();
		postMap.put("question", question);
		if(anonymous) postMap.put("anonymous", "1");
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, true, postMap);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to retrieve profile details about a user.
	 * @param username The username to query, or null to query about the current user.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse profileDetails(String username) {
		//Formulate the url to call
		String url;
		
		if(username == null) { //query about the current user.
			url = "/profile/details.xml";
		} else {
			url = "/profile/details/" + username + ".xml";
		}
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Queries the FormspringAPI to search for a specific user.
	 * @param query The query text.
	 * @param page The result page number.
	 * @return The FormspringAPIResponse with the result from the API call.
	 */
	public FormspringAPIResponse searchProfiles(String query, int page) {
		//Formulate the url to call
		String url = "/search/profiles.xml?query=" + query + "&page=" + page;
		
		//Run the API call and retrieve the string result
		String result = this.doApiRequest(url, false, null);
		
		//Inflate the response and return it.
		return this.inflateResponse(result);
	}
	
	/**
	 * Performs an API request. If caching is enabled, this method will first check for
	 * the existance of a valid cache file for the particular request.
	 * @param path The path on the server to access.
	 * @param post Is this a post request?
	 * @param postVars The post variables to send with the request. Null if post is false.
	 * @return The text of the response from the server.
	 */
	private String doApiRequest(String path, boolean post, Map<String, String> postVars) {
		//Generate an md5 hash on the request URL.
		String md5Path = this.md5(path);
		
		//Caching is enabled, this is not a POST.
		if(this.isCaching && post == false) {
			//Does such a file exist in our cache path?
			File cacheFile = new File(this.cacheDir, md5Path);
			
			//Does this file exist?
			if(cacheFile.exists()) {
				//If so, let's see how old it is.
				long lastModifiedTime = cacheFile.lastModified();
				long currentSystemTime = System.currentTimeMillis();
				
				//Currently, we want to make sure that the cache file in question is less
				//than 10 minutes old. This is for API requests. For images and the like
				//that are cached by various AsyncTasks we'd choose a much higer value.
				if(currentSystemTime - lastModifiedTime < 600000) {
					//This cache file is less than 10 minutes old, return the contents of
					//that file as the API response to avoid hitting the server.
					//Start up a buffered reader and read the contents into a return
					//string utility variable.
					//Log.i("FormspringAPI", "Valid cache file found. Using cache file.");
					String rtnString = "";
					String line;
					
					try {
						//Start up the Buffered Reader, read in the first line.
						BufferedReader buf = new BufferedReader(new FileReader(cacheFile));
						line = buf.readLine();
						
						//Until we hit the end of the reader, add the lines to the
						//return string.
						while(line != null) {
							rtnString += line;
							line = buf.readLine();
						}
						
						//When that is done, close the buffered reader and return the
						//result
						buf.close();
						return rtnString;
					} catch (FileNotFoundException e) {
						//If we hit an exception, print a stack trace.
						e.printStackTrace();
					} catch (IOException e) {
						//If we hit an IOException, print a stack trace.
						e.printStackTrace();
					}
				} else {
					//Log.i("FormspringAPI", "Expired cache file found. Running API call.");
				}
			}
		}
		
		//No cache file was located, caching is disabled, or this is an HTTP
		//POST request, and caching doesn't apply.
		try {
			//Build the full URL and and open the HttpURLConnection object.
			//URL targetUrl = new URL(FormspringAPI.apiUrl + path);
			//HttpURLConnection request = (HttpURLConnection) targetUrl.openConnection();
			Request scribeRequest;
			
			//Determine if this is a POST request.
			if(post == true) {
				//Set the HTTP connection to a POST method.
				scribeRequest = new Request(Verb.POST, FormspringAPI.apiUrl + path);
				
				//Add post parameters.
				String[] postKeys = (String[]) postVars.keySet().toArray();
				
				//For each of the keys
				for(String key : postKeys) {
					scribeRequest.addBodyParameter(key, postVars.get(key));
				}
			} else {
				scribeRequest = new Request(Verb.GET, FormspringAPI.apiUrl + path);
			}
			
			//If we are authroized, sign the request
			if(this.isAuthed) {
				scribeObj.signRequest(scribeRequest, accessToken);
			}
			
			//Run the request.
			Response scribeResponse = scribeRequest.send();
			
			//Pull out the response text into a string.
			String apiResponse = scribeResponse.getBody();
			
			//Open up a file writer to save the response to disk, if caching is
			//enabled and this is not a post.
			if(this.isCaching && post == false) {
				File cacheFile = new File(this.cacheDir, md5Path);
				
				if(cacheFile.canWrite() || cacheFile.createNewFile()) {
					//Open up a buffered writer and get down to business.
					BufferedWriter bufw = new BufferedWriter(new FileWriter(cacheFile));
					
					//Write out the API response to the file.
					bufw.write(apiResponse);
					
					//Close the buffer.
					bufw.close();
					
					//Display a log message.
					//Log.i("FormspringAPI", "This API response has been cached.");
				} else {
					//Log.e("FormspringAPI", "I don't have write permission to that cache file.");
				}
			}
			
			//Return the API response
			return apiResponse;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	/**
	 * Uses the Simple XML library to inflate the XML from the server into objects
	 * the API can use.
	 * @param xml The string of XML.
	 * @return A FormspringAPIResponse object generated from the XML.
	 */
	protected FormspringAPIResponse inflateResponse(String xml) {
		//System.out.println("GO: " + xml);
		try {
			//Use simplexml to inflate the result into a FormspringAPIResponse object.
			FormspringAPIResponse response = serializer.read(FormspringAPIResponse.class, xml);
			
			//Return the API response
			return response;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * @return the accessToken
	 */
	public Token getAccessToken() {
		return accessToken;
	}

	/**
	 * @return the currentUsersName
	 */
	public String getCurrentUsersName() {
		return currentUsersName;
	}

	/**
	 * @return the currentUser
	 */
	public FormspringPerson getCurrentUser() {
		return currentUser;
	}
	
	/**
	 * Utility method for generating an md5 hash of an arbitrary string.
	 * @param s
	 * @return
	 */
	private String md5(String s) {
	    try {
	        // Create MD5 Hash
	        MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
	        digest.update(s.getBytes());
	        byte messageDigest[] = digest.digest();
	        
	        // Create Hex String
	        StringBuffer hexString = new StringBuffer();
	        for (int i=0; i<messageDigest.length; i++)
	            hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
	        return hexString.toString();
	        
	    } catch (NoSuchAlgorithmException e) {
	        e.printStackTrace();
	    }
	    return "";
	}
}
